#include "transport.h"
#include "method_thread.h"
#include "timeutils.h"
#include "smalloc.h"
#include "trace.h"

#include <stdint.h>
#include <gloox/base64.h>
#include <gloox/connectiontcpclient.h>
#include <gloox/connectionhttpproxy.h>
#include <sstream>
#include <cstring>
#include <cstdio>

// TODO low and high water mark

namespace {
    bool http_proxy_config(std::string *host, int *port)
    {
        char *tmp;
        if ((tmp = getenv("http_proxy")) != NULL) {
            std::string proxy_config(tmp);
            // http://h:p
            if (proxy_config.size() >= 10 &&
                    proxy_config.substr(0, 7) == "http://") {
                size_t colon;
                if ((colon = proxy_config.rfind(':')) != std::string::npos) {
                    *host = proxy_config.substr(7, colon - 7);
                    *port = atoi(proxy_config.substr(colon + 1).c_str());
                    return true;
                }
            }
        }
        return false;    
    }
}

transport::transport(const std::string &id)
    : id_(id)
{
}

transport::~transport()
{
    std::vector<transport_listener *>::const_iterator it, end;
    for (it = listeners_.begin(), end = listeners_.end(); it != end; ++it) {
        delete *it;
    }
    listeners_.clear();
}

std::string transport::id() const
{
    return id_;
}

void transport::add_listener(transport_listener *l)
{
    listeners_.push_back(l);
}

stransport_lite::stransport_lite(transport *trans)
    : transport(trans->id()), trans_(trans), secure_(NULL)
{

}

stransport_lite::~stransport_lite()
{
    delete trans_;
    delete secure_;
}

bool stransport_lite::connect(const std::vector<std::string> &options)
{
    bool r = false;
    // ugly param spec: the first option is supposed to be a colon-separated 
    // string and is used to initialize the security manager
    if (options.size() != 0) {
        std::istringstream iss(options[0]);
        std::string op;
        std::vector<std::string> sec_opts;
        while (getline(iss, op, ':')) {
            sec_opts.push_back(op);
        }
        if (sec_opts.size() != 3) {
            TERR("invalid security options: %s", options[0].c_str());
        } else {
            secure_ = new security_manager(sec_opts[0], sec_opts[1],
                    sec_opts[2]);
            std::vector<std::string> opts(options.begin() + 1, options.end());
            if (trans_) {
                if ((r = trans_->connect(opts))) {
                    trans_->add_listener(this);
                }
            }
        }
    }
    return r;
}

bool stransport_lite::ucast(const std::string &to, const std::string &data)
{
    std::string signed_data;
    if (sign(data, &signed_data)) {
        return trans_->ucast(to, signed_data);
    }
    return false;
}

bool stransport_lite::bcast(const std::string &data)
{
    std::string signed_data;
    if (sign(data, &signed_data)) {
        return trans_->bcast(signed_data);
    }
    return false;
}

bool stransport_lite::close()
{
    if (trans_) {
        return trans_->close();
    }
    return false;
}

bool stransport_lite::sign(const std::string &data, std::string *signed_data)
{
    bool r = false;
    void *sig;
    unsigned sig_size;
    if (secure_->sign(data.data(), data.size(), &sig, &sig_size)) {
        // prepend signature
        unsigned sz = sizeof(sig_size) + sig_size + data.size();
        char *buf = new char[sz];
        memcpy(buf, &sig_size, sizeof(sig_size));
        memcpy(buf + sizeof(sig_size), sig, sig_size);
        memcpy(buf + sz - data.size(), data.data(), data.size());
        signed_data->assign(buf, sz);
        delete [] buf;
        r = true;
    } else {
        TERR("failed to sign the data");
    }
    free(sig);
    return r;
}

void stransport_lite::on_data(const std::string &src, const std::string &data)
{
    if (!listeners_.empty()) {
        // first read the signature
        const char *raw_data = data.data();
        uint32_t sig_size;
        char *sig;
        memcpy(&sig_size, raw_data, sizeof(sig_size));
        sig = new char[sig_size];
        memcpy(sig, raw_data + sizeof(sig_size), sig_size);
        std::string body = data.substr(sig_size + sizeof(sig_size));
        if (secure_->verify(body.data(), body.size(), sig, sig_size,
                    src.c_str())) {
            std::vector<transport_listener *>::const_iterator it, end;
            it = listeners_.begin();
            end = listeners_.end();
            for (; it != end; ++it) {
                (*it)->on_data(src, body);
            }
        } else {
            TWARN("cannot trust data from %s", src.c_str());
        }

        delete [] sig;
    }
}

xmpp_transport::xmpp_transport(const std::string &id)
    : transport(id), state_(DISCONNECTED)
{
    pthread_mutex_init(&sending_m_, NULL);
    pthread_mutex_init(&connect_m_, NULL);
    pthread_cond_init(&connected_cv_, NULL);
}

xmpp_transport::~xmpp_transport()
{
    pthread_mutex_destroy(&sending_m_);
    pthread_mutex_destroy(&connect_m_);
    pthread_cond_destroy(&connected_cv_);
}

bool xmpp_transport::connect(const std::vector<std::string> &options)
{
    if (state_ == CONNECTED) {
        return true;
    }
    std::string jserver = "";
    std::string passwd = "";
    bool notls = false;
    int optc = options.size() + 1;
    char **opt_array = (char **)smalloc(sizeof(char *) * optc);
    opt_array[0] = (char *) smalloc(sizeof(char) * 2);
    strcpy(opt_array[0], "a"); // non-sense letter
    for (int i = 1; i < optc; ++i) {
        opt_array[i] = (char *) smalloc(sizeof(char) *
                (options[i - 1].size() + 1));
        strcpy(opt_array[i], options[i - 1].c_str());
    }
    int opt;
    while ((opt = getopt(optc, opt_array, "sp:h:")) != -1) {
        switch (opt) {
        case 'p':
            passwd = optarg;
            break;
        case 'h':
            jserver = optarg;
            break;
        case 's':
            notls = true;
        default:
            break;
        }
    }
    size_t at_symbol;
    if (jserver == "" && (at_symbol = id_.rfind('@')) != std::string::npos) {
        jserver = id_.substr(at_symbol + 1);
    }
    if (jserver != "") {
        pthread_mutex_lock(&connect_m_);
        gloox::JID jid(id_);
        client_ = new gloox::Client(jid, passwd);
        if (notls) {
            TINFO("turning off TLS for xmpp");
            client_->setTls(gloox::TLSDisabled);
        }
        std::string httpproxy_host;
        int httpproxy_port;
        if (http_proxy_config(&httpproxy_host, &httpproxy_port)) {
            gloox::ConnectionTCPClient *conn0 = new gloox::ConnectionTCPClient(
                    client_->logInstance(), httpproxy_host, httpproxy_port);
            gloox::ConnectionHTTPProxy *conn1 = new gloox::ConnectionHTTPProxy(
                    client_, conn0, client_->logInstance(), jserver);
            client_->setConnectionImpl(conn1);
        }
        client_->setServer(jserver);
        client_->registerMessageHandler(this);
        client_->registerConnectionListener(this);
        struct timespec now, timeout;
        clock_gettime(CLOCK_REALTIME, &now);
        add_timespec(now, 30000, &timeout);
        if (client_->connect(false)) {
            state_ = CONNECTING;
            assert((recv_th_ = method_thread(this, false,
                    &xmpp_transport::receive)) != 0);
            while (state_ != CONNECTED) {
                if (pthread_cond_timedwait(&connected_cv_, &connect_m_,
                            &timeout) == ETIMEDOUT) {
                    const char *jcstr = jserver.c_str();
                    TERR("connection to %s timed out", jcstr);
                    break;
                }
            }
        } else {
            TERR("xmpp connection failed");
        }
        pthread_mutex_unlock(&connect_m_);
    } else {
        TERR("either server or password unspecified");
    }

    for (int i = 0; i < optc; ++i) {
        free(opt_array[i]);
    }
    free(opt_array);

    return state_ == CONNECTED;
}

bool xmpp_transport::ucast(const std::string &to, const std::string &data)
{
    pthread_mutex_lock(&sending_m_);
    std::string body = gloox::Base64::encode64(data);
    gloox::Message msg(gloox::Message::Chat, gloox::JID(to), body);
    client_->send(msg);
    pthread_mutex_unlock(&sending_m_);
    return true;
}

bool xmpp_transport::bcast(const std::string &data)
{
    return false;
}

bool xmpp_transport::close()
{
    state_ = DISCONNECTED;
    client_->disconnect();
    pthread_join(recv_th_, NULL);

    if (!listeners_.empty()) {
        std::vector<transport_listener *>::const_iterator it, end;
        for (it = listeners_.begin(), end = listeners_.end(); it != end; ++it) {
            (*it)->on_close();
        }
    }

    return 0;
}

void xmpp_transport::receive()
{
    while (state_ != DISCONNECTED) {
        if (client_->recv(10) != gloox::ConnNoError) {
            break;
        }
    }
}

void xmpp_transport::onConnect()
{
    pthread_mutex_lock(&connect_m_);
    assert(state_ == CONNECTING);
    state_ = CONNECTED;
    pthread_cond_signal(&connected_cv_);
    pthread_mutex_unlock(&connect_m_);
}

bool xmpp_transport::onTLSConnect(const gloox::CertInfo &info)
{
    return true;
}

void xmpp_transport::onDisconnect(gloox::ConnectionError e)
{
    if (e == gloox::ConnNoError) {
        TDEBUG("Disconnect normally\n");
    } else if (e == gloox::ConnStreamError) {
        TERR("Stream error: %d", client_->streamError());
    } else {
        gloox::ConnectionError status = gloox::ConnNoError;
        while (status == gloox::ConnNoError) {
            status = client_->recv();
        }
        TERR("xmpp disconnected with code %d", status);
    }
}

void xmpp_transport::handleMessage(const gloox::Message &stanza,
        gloox::MessageSession *session)
{
    if (!listeners_.empty()) {
        if (stanza.subtype() == gloox::Message::Chat) {
            const std::string &body = stanza.body();
            const std::string &src = stanza.from().bare();
            std::vector<transport_listener *>::const_iterator it, end;
            it = listeners_.begin();
            end = listeners_.end();
            for (; it != end; ++it) {
                (*it)->on_data(src, gloox::Base64::decode64(body));
            }
        }
    }
}

